热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

6000字|统一缓存帝国实战SpringCache

6000,字,统一,缓存,帝国,实战,sp

回复 PDF 领取架构师资料 

这是悟空的第 110 篇原创文章

作者 | 悟空聊架构

来源 | 悟空聊架构(ID:PassJava666)

你好,我是悟空。

前三篇讲解的缓存使用和分布式锁的都是基于 Redis 来做缓存的,本篇我来向大家介绍一种兼容所有缓存中间件的方案,不论我们是使用 Redis 还是 Ehcache,都不需要关心如何操作 Redis 或者 Ehcache,这套方案统统帮你搞定。

这套方案就是大名鼎鼎的 Spring Cache。什么?你没有听过,没关系,本篇带你一起探索。

缓存系列文章:

《缓存实战(一)》

《缓存实战(二)Redis分布式锁》

《缓存实战(三)Redisson 分布式锁》

一、揭开 Spring Cache 的面纱

1.1 现有缓存方案的痛点

试想一种场景

1.用户 A 打开 APP,进入到了秒杀商品的详情页,那这个商品数据我们会先去数据库查询,然后返回给客户端。

2.因为有大量用户短时间内进入到了详情页,所以可以把活动列表缓存起来,直接读缓存就可以了。

3.那下次再查询商品时,直接去缓存查询就可以了。如果秒杀商品下架了,缓存的数据不会用到了,就把缓存删掉就可以了。

4.上面几步看起来也没啥问题,但是放缓存,删除缓存这两步是需要我们去手动写代码实现的。有没有一种方式不用写操作缓存的代码?

5.假如现在用的缓存中间件是 Redis,领导说要换成 Ehcache,操作缓存的代码是不是又得重新撸一遍?

总结下上面场景的痛点:

  • 需要手写操作缓存代码,如添加缓存、更新缓存、删除缓存。

  • 切换缓存组件并不容易,或者说没有对缓存层进行抽象封装,依赖具体的缓存中间件。

哪有没有一种方案可以帮助解决上面的两个痛点呢?

这就是今天要介绍的 Spring Cache

1.2 Spring Cache 介绍

Spring Cache 是 Spring 提供的一整套的缓存解决方案。虽然它本身并没有提供缓存的实现,但是它提供了一整套的接口和代码规范、配置、注解等,这样它就可以整合各种缓存方案了,比如 Redis、Ehcache,我们也就不用关心操作缓存的细节。

Spring 3.1 开始定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术,并支持使用注解来简化我们开发。

Cache 接口它包含了缓存的各种操作方式,同时还提供了各种xxxCache缓存的实现,比如 RedisCache 针对Redis,EhCacheCache 针对 EhCache,ConcurrentMapCache 针对 ConCurrentMap,具体有哪几种,后面实战中会介绍。

1.3 Spring Cache 有什么功效

每次调用某方法,而此方法又是带有缓存功能时,Spring 框架就会检查指定参数的那个方法是否已经被调用过,如果之前调用过,就从缓存中取之前调用的结果;如果没有调用过,则再调用一次这个方法,并缓存结果,然后再返回结果,那下次调用这个方法时,就可以直接从缓存中获取结果了。

1.4 Spring Cache 的原理是什么?

Spring Cache 主要是作用在类上或者方法上,对类中的方法的返回结果进行缓存。那么如何对方法增强,来实现缓存的功能?

学过 Spring 的同学,肯定能一下子就反应过来,就是用 AOP(面向切面编程)。

面向切面编程可以简单地理解为在类上或者方法前加一些说明,就是我们常说的注解。

Spring Cache 的注解会帮忙在方法上创建一个切面(aspect),并触发缓存注解的切点(poinitcut),听起来太绕了,简单点说就是:Spring Cache 的注解会帮忙在调用方法之后,去缓存方法调用的最终结果,或者在方法调用之前拿缓存中的结果,或者删除缓存中的结果,这些读、写、删缓存的脏活都交给 Spring Cache 来做了,是不是很爽,再也不用自己去写缓存操作的逻辑了。

1.5 缓存注解

Spring 提供了四个注解来声明缓存规则。@Cacheable,@CachePut,@CacheEvict,@Caching。

大家先有个概念,后面我们再来看怎么使用这些缓存注解。

二、使用缓存

2.1 引入 Spring Cache 依赖

在 pom 文件中引入 spring cache 依赖,如下所示:

 <dependency>     <groupId>org.springframework.bootgroupId>     <artifactId>spring-boot-starter-cacheartifactId> dependency> 

2.2 配置使用哪种缓存

Spring Cache 支持很多缓存中间件作为框架中的缓存,总共有 9 种选择:

  • caffeine:Caffeine 是一种高性能的缓存库,基于 Google Guava。

  • couchbase: CouchBase是一款非关系型JSON文档数据库。

  • generic:由泛型机制和 static 组合实现的泛型缓存机制。

  • hazelcast:一个高度可扩展的数据分发和集群平台,可用于实现分布式数据存储、数据缓存。

  • infinispan:分布式的集群缓存系统。

  • jcache:JCache 作为缓存。它是 JSR107 规范中提到的缓存规范。

  • none:没有缓存。

  • redis:用 Redis 作为缓存

  • simple:用内存作为缓存。

mark

我们还是用最熟悉的 Redis 作为缓存吧。配置 Redis 作为缓存也很简单,在配置文件 application.properties 中设置缓存的类型为 Redis 就可以了, 如下所示:

当然,别忘了还要在 pom 文件中 引入 Redis 的依赖,不然用不了 Redis。

 <dependency>     <groupId>org.springframework.bootgroupId>     <artifactId>spring-boot-starter-data-redisartifactId> dependency> 

2.3 测试缓存

那基础的配置已经做好了,现在就是看怎么使用 Spring Cache 了。

(1)启动类上添加 @EnableCaching注解。本文案例就是在 启动类 PassjavaQuestionApplication 添加 @EnableCaching注解。

(2)指定某方法开启缓存功能。在方法上添加 @Cacheable 缓存注解就可以了。

@Cacheable 注解中,可以添加四种参数:value,key,condition,unless。首先我们来看下 value 参数。

下面的代码出于演示作用,用了最简单的逻辑,test 方法直接返回一个数字,连数据库查询都没有做。不过没关系,我们主要验证 Spring Cache 是否对方法的结果进行了缓存。

 @RequestMapping("/test") @Cacheable({"hot"}) public int test() {     return 222; } 

大家注意,@Cacheable 注解中小括号里面还含有大括号,大括号里面还有 “hot” 字符串,这个 hot 字符串你可以把它当作一个缓存的名字,然后将 test 方法返回的结果存到 hot 缓存中。我们也可以用 value="hot" 的方式。

第一次调用 test 方法前,既没有 hot 缓存,更没有 test 的结果缓存。

调用 test 方法后,Redis 中就创建出了 hot 缓存了,然后缓存了一个 key,如下图所示:

第二次调用 test 方法时,就从缓存 hot 中将 test 方法缓存的结果 222 取出来了,为了验证没有执行 test 中的方法,大家可以在 test 方法中打下 log 或者断点。最后的验证结果肯定是没有走 test 方法的,而是直接从缓存中获取的。

那我们再来测试一个方法,方法名改为 test2,且请求路径也改为 test2 了。

 @RequestMapping("/test2") @Cacheable({"hot"}) public int test2() {     return 456; } 

大家觉得这两个方法的结果都会缓存吗?还是只会缓存第一个被调用的方法?

经过测试,执行第一个 test 方法后,再执行 test2 方法,缓存结果一直是 222 不会变。因为他们的 key 都是 默认的 SimpleKey[],所以两个方法对应的缓存的 key 都叫这个,所以得到的缓存值是一样的。

(3)加上数据库查询的测试。

有的同学可能觉得上面的测试太简单了,test 方法里面啥都没做,还缓存啥呢,完全没必要啊。没关系,大家的顾虑是对的,我们来加上数据库查询,安排~

先说下场景:前端需要查询某个题目的详情,正常逻辑是查询数据库后返回结果。假定这个查询操作非常频繁,我们需要将题目详情进行缓存。我们先看看常规 Redis 缓存方案:

先从 Redis 缓存中查看缓存中是否有该题目,如果缓存中有,则返回缓存中的题目;如果没有,就从数据库中查。查询出题目后,就用 Redis 存起来,然后返回。这里就要写操作 Redis 的代码了:查询 Redis 缓存、更新 Redis 缓存。

 // 查询缓存,假定该题目详情缓存的 key=question1 redisTemplate.opsForValue().get("question1");  // 更新缓存 redisTemplate.opsForValue().set("question1", questionEntity); 

那如果用 Spring Cache 注解的话,上面两行代码可以直接干掉了。如下所示,加一个 @Cacheable 注解搞定。

 @Cacheable({"question""hot"}) public QuestionEntity info(Long id) {     return getById(id); // 查询数据库操作 } 

其中 question 和 hot 是缓存的名字,我们可以将结果放到不同的缓存中。

结论:

  • 如果没有指定请求参数,则缓存生成的 key name,是默认自动生成的,叫做 SimpleKey[]。

  • 如果指定了请求参数,则缓存的 key  name 就是请求参数,比如上面 info 方法,key 等于我传入的 id = 1。

  • 缓存中 key 对应的 value 默认使用 JDK 序列化后的数据。

  • value 的过期时间为 -1,表示永不过期。

2.4 自定义配置类

上面保存的缓存数据都是默认设置,我们也可以自己定义配置,如下所示,在配置文件 application.properties 添加如下配置:

 # 使用 Redis 作为缓存组件 spring.cache.type=redis # 缓存过期时间为 3600s spring.cache.redis.time-to-live=3600000 # 缓存的键的名字前缀 spring.cache.redis.key-prefix=passjava_ # 是否使用缓存前缀 spring.cache.redis.use-key-prefix=true # 是否缓存控制,防止缓存穿透 spring.cache.redis.cache-null-values=true 

然后需要加一个配置类:MyCacheConfig。可以在我的开源项目 passjava 获取完整源码。

 RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { } 

2.5 自定义 key

然后我们可以指定 key 的值,可以在 @Cacheable 注解里面加上 key 的值 #root.method.name。这是一种特有的表达式,称作 SpEL 表达式,这里代表用方法名作为缓存 key 的名字。

 @Cacheable(value = {"hot"}, key = "#root.method.name") 

接下来就是见证奇迹的时刻,调用 test 方法和 test2 方法,发现有两个不同的 key,一个是 passjava_test1,另外一个 passjava_test2,它们的 key 就是前缀 passjava_ + 方法名 组成。

SpEL 表达式还有很多其它规则,如下所示:

可以根据项目需要选择合适的表达式来自定义 key。

2.6 自定义条件

除了设置缓存条目的 key,我们还可以自定义条件来决定是否将缓存功能关闭。这里就要用到@Cacheable 另外两个属性:condition 和 unless,它俩的格式还是用 SpEL 表达式。对应的四个属性总结如下:

代码示例如下:

 @Cacheable(value = "hot", unless = "#result.message.containss('NoCache')") 

当放回的结果 message 字段包含有 NoCache 就不会进行缓存。

2.7 更新注解

@CachePut 也是用来更新缓存,和 @Cacheable 非常相似,不同点是 @CachePut 注解的方法始终都会执行,返回值也会也会放到缓存中。通常用在保存的方法上。

保存成功后,可以将 key 设置保存实例的 id。这个怎么做呢?

之前我们说过 key 可以通过 SpEL 表达式来指定,这里就可以搭配 #result.id 来实现。

这里还是用个例子来说明用法:创建题目的方法,返回题目实例,其中包含有题目 id。

 @RequestMapping("/create") @CachePut(value = "hot", key = "#result.id") public QuestionEntity create(@Valid @RequestBody QuestionEntity question){     return IQuestionService.createQuestion(question); } 

保存的 id 是自增的,值为 123,所以缓存中的 key = passjava_123。

2.8 删除缓存注解

@CacheEvict 注解的方法在调用时不会在缓存中添加任何东西,但是会从从缓存中移除之前的缓存结果。

示例代码如下:

 @RequestMapping("/remove/{id}") @CacheEvict(value = "hot") public R remove(@PathVariable("id") Long id){     IQuestionService.removeById(id);     return R.ok(); } 

删除条目的 key 与传递进来的 id 相同。我测试的时候传的 id = 123,经过前缀passjava_组装后就是 passjava_123,所以将之前缓存的 passjava_123 删除了。重复执行也不会报错。

注意:@CacheEvict 和 @Cacheable、@CachePut 不同,它能够应用在返回值为 void 的方法上。

@CacheEvict 还有些属性可供使用,总结如下:

三、 总结

本文通过传统使用缓存的方式的痛点引出 Spring 框架中的 Cache 组件。然后详细介绍了 Spring Cache 组件的用法:

  • 五大注解。@Cacheable、@CachePut、@CacheEvict、@Caching,、@CacheConfig。

  • 如何自定义缓存条目的 key。

  • 如何自定义 Cache 配置。

  • 如何自定义缓存的条件。

当然 Spring Cache 并不是万能的,缓存一致性问题依旧存在,下一篇,我们再细聊缓存的一致性问题。

缓存系列文章汇总

《缓存实战(一)》

《缓存实战(二)Redis分布式锁》

《缓存实战(三)Redisson 分布式锁》

参考资料:

www.passjava.cn 

Spring in Action

- END -

写了两本 PDF, 回复  分布式  或  PDF  载。

我的 JVM 专栏已上架,回复  JVM  领取

 

我是悟空,努力变强,变身超级赛亚人!

本文分享自微信公众号 - 悟空聊架构(PassJava666)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。


推荐阅读
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • Linux如何安装Mongodb的详细步骤和注意事项
    本文介绍了Linux如何安装Mongodb的详细步骤和注意事项,同时介绍了Mongodb的特点和优势。Mongodb是一个开源的数据库,适用于各种规模的企业和各类应用程序。它具有灵活的数据模式和高性能的数据读写操作,能够提高企业的敏捷性和可扩展性。文章还提供了Mongodb的下载安装包地址。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 一句话解决高并发的核心原则
    本文介绍了解决高并发的核心原则,即将用户访问请求尽量往前推,避免访问CDN、静态服务器、动态服务器、数据库和存储,从而实现高性能、高并发、高可扩展的网站架构。同时提到了Google的成功案例,以及适用于千万级别PV站和亿级PV网站的架构层次。 ... [详细]
  • 本文讨论了在使用Git进行版本控制时,如何提供类似CVS中自动增加版本号的功能。作者介绍了Git中的其他版本表示方式,如git describe命令,并提供了使用这些表示方式来确定文件更新情况的示例。此外,文章还介绍了启用$Id:$功能的方法,并讨论了一些开发者在使用Git时的需求和使用场景。 ... [详细]
  • 1Lock与ReadWriteLock1.1LockpublicinterfaceLock{voidlock();voidlockInterruptibl ... [详细]
  • GO语言 包 if..else.. for循环 switch 数组
    包1.什么是包1.新建一个文件夹,内部写很多go文件,但是包名必须一致,改文件夹就是一个包2.作用和优点包用于组织Go源代码,提供了更好的可重用性与可读性。由于包提供了代码的封装, ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • flowable工作流 流程变量_信也科技工作流平台的技术实践
    1背景随着公司业务发展及内部业务流程诉求的增长,目前信息化系统不能够很好满足期望,主要体现如下:目前OA流程引擎无法满足企业特定业务流程需求,且移动端体 ... [详细]
  • 由于同源策略的限制,满足同源的脚本才可以获取资源。虽然这样有助于保障网络安全,但另一方面也限制了资源的使用。那么如何实现跨域呢,以下是实现跨域的一些方法。 ... [详细]
  • 详解 Python 的二元算术运算,为什么说减法只是语法糖?[Python常见问题]
    原题|UnravellingbinaryarithmeticoperationsinPython作者|BrettCannon译者|豌豆花下猫(“Python猫 ... [详细]
  • 移动传感器扫描覆盖摘要:关于传感器网络中的地址覆盖问题,已经做过很多尝试。他们通常归为两类,全覆盖和栅栏覆盖,统称为静态覆盖 ... [详细]
  • 【MyBatis系列7】原来SqlSession只是个甩手掌柜,真正干活的却是Executor等四大对象
    Executor原理分析前言MyBatis架构分层ExecutorBaseExecutorSimpleExecutorReuseExecutorBatchExecutor三种常用批 ... [详细]
author-avatar
手机用户2502913925
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有